package com.study.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.study.cacheManager.LinkStatsCacheManager;
import com.study.constant.RedissonLockConstant;
import com.study.convention.errorCode.BaseErrorCode;
import com.study.convention.exception.ServiceException;
import com.study.dao.entity.*;
import com.study.dao.mapper.*;
import com.study.dto.biz.ShortLinkStatsRecordDTO;
import com.study.dto.req.ShortLinkGroupStatsAccessRecordReqDTO;
import com.study.dto.req.ShortLinkGroupStatsReqDTO;
import com.study.dto.req.ShortLinkStatsAccessRecordReqDTO;
import com.study.dto.req.ShortLinkStatsReqDTO;
import com.study.dto.resp.ShortLinkStatsAccessRecordRespDTO;
import com.study.dto.resp.ShortLinkStatsRespDTO;
import com.study.service.ShortLinkStatsService;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
 * @author canace
 * @version 1.0
 * @description 短链接监控服务实现类
 * @date 2024/2/4 16:38
 */

@Service
@RequiredArgsConstructor
public class ShortLinkStatsServiceImpl implements ShortLinkStatsService {

    private final RedissonClient redissonClient;

    private final TLinkAccessStatsMapper linkAccessStatsMapper;
    private final TLinkLocaleStatsMapper linkLocaleStatsMapper;
    private final TLinkAccessLogsMapper linkAccessLogsMapper;
    private final TLinkBrowserStatsMapper linkBrowserStatsMapper;
    private final TLinkOsStatsMapper linkOsStatsMapper;
    private final TLinkDeviceStatsMapper linkDeviceStatsMapper;
    private final TLinkNetworkStatsMapper linkNetworkStatsMapper;
    private final TLinkStatsTodayMapper linkStatsTodayMapper;

    private final LinkStatsCacheManager linkStatsCacheManager;

    private final TLinkGotoMapper linkGotoMapper;
    private final TLinkMapper linkMapper;

    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Value("${short-link.stats.remote.url}")
    private String statsRemoteUrl;

    @Override
    public ShortLinkStatsRespDTO shortLinkStats(ShortLinkStatsReqDTO reqDTO) {
        return linkStatsCacheManager.shortLinkStats(reqDTO);
    }

    @Transactional
    @Override
    public void insertShortLinkStats(String fullShortUrl, String gid, ShortLinkStatsRecordDTO statsRecord) {
        // 取得完整短链接
        fullShortUrl = Optional.ofNullable(fullShortUrl).orElse(statsRecord.getFullShortUrl());
        // 如果gid为空，则从数据库中获取gid
        if (StrUtil.isBlank(gid)) {
            LambdaQueryWrapper<TLinkGoto> linkGotoLambdaQueryWrapper = Wrappers.lambdaQuery(TLinkGoto.class).eq(TLinkGoto::getFullShortUrl, fullShortUrl);
            TLinkGoto tLinkGoto = linkGotoMapper.selectOne(linkGotoLambdaQueryWrapper);
            if (ObjectUtil.isEmpty(tLinkGoto)) {
                throw new ServiceException(BaseErrorCode.SHORT_LINK_IS_NOT_EXIST_ERROR);
            }
            gid = tLinkGoto.getGid();
        }
        // 封装基础访问数据
        // 获取当前的时间
        int hour = DateUtil.hour(new Date(), true);
        Week week = DateUtil.dayOfWeekEnum(new Date());
        int weekValue = week.getIso8601Value();
        TLinkAccessStats linkAccessStats = TLinkAccessStats.builder().fullShortUrl(fullShortUrl)
                .gid(gid)
                .uv(statsRecord.getUvFirstFlag() ? 1 : 0)
                .pv(1)
                .uip(statsRecord.getUipFirstFlag() ? 1 : 0)
                .hour(hour)
                .weekday(weekValue)
                .date(new Date()).build();

        // 根据ip地址封装地区
        Map<String, Object> localeParamMap = new HashMap<>();
        localeParamMap.put("key", statsLocaleAmapKey);
        localeParamMap.put("ip", statsRecord.getRemoteAddr());
        // 调用高德api获取地区信息
        String localeResultStr = HttpUtil.get(statsRemoteUrl, localeParamMap);
        JSONObject localeResultObj = JSON.parseObject(localeResultStr);
        String infoCode = localeResultObj.getString("infocode");
        String actualProvince = "未知";
        String actualCity = "未知";
        if (StrUtil.isBlank(infoCode) || !"10000".equals(infoCode)) {
            throw new ServiceException(BaseErrorCode.STATS_REMOTE_ERROR);
        }
        String province = localeResultObj.getString("province");
        boolean unknownFlag = StrUtil.equals(province, "[]");
        TLinkLocaleStats linkLocaleStats = TLinkLocaleStats.builder()
                .province(actualProvince = unknownFlag ? actualProvince : province)
                .city(actualCity = unknownFlag ? actualCity : localeResultObj.getString("city"))
                .adcode(unknownFlag ? "未知" : localeResultObj.getString("adcode"))
                .cnt(1)
                .fullShortUrl(fullShortUrl)
                .country("中国")
                .gid(gid)
                .date(new Date())
                .build();

        // 封装系统os
        TLinkOsStats linkOsStats = TLinkOsStats.builder().fullShortUrl(fullShortUrl)
                .gid(gid)
                .os(statsRecord.getOs())
                .cnt(1)
                .date(new Date()).build();


        // 封装浏览器
        TLinkBrowserStats linkBrowserStats = TLinkBrowserStats.builder().fullShortUrl(fullShortUrl)
                .gid(gid)
                .browser(statsRecord.getBrowser())
                .cnt(1)
                .date(new Date()).build();

        // 封装设备
        TLinkDeviceStats linkDeviceStats = TLinkDeviceStats.builder().fullShortUrl(fullShortUrl)
                .gid(gid)
                .device(statsRecord.getDevice())
                .cnt(1)
                .date(new Date()).build();

        // 封装网络
        TLinkNetworkStats linkNetworkStats = TLinkNetworkStats.builder().fullShortUrl(fullShortUrl)
                .gid(gid)
                .network(statsRecord.getNetwork())
                .cnt(1)
                .date(new Date()).build();

        // 封装访问日志
        TLinkAccessLogs linkAccessLogs = TLinkAccessLogs.builder().fullShortUrl(fullShortUrl)
                .user(statsRecord.getUv())
                .gid(gid)
                .ip(statsRecord.getRemoteAddr())
                .browser(statsRecord.getBrowser())
                .device(statsRecord.getDevice())
                .os(statsRecord.getOs())
                .network(statsRecord.getNetwork())
                .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                .build();

        // 封装今日访问统计
        TLinkStatsToday linkStatsToday = TLinkStatsToday.builder().fullShortUrl(fullShortUrl)
                .todayPv(1)
                .todayUv(statsRecord.getUvFirstFlag() ? 1 : 0)
                .todayUip(statsRecord.getUipFirstFlag() ? 1 : 0)
                .gid(gid)
                .fullShortUrl(fullShortUrl)
                .date(new Date())
                .build();

        // 将上述数据插入数据库
        // 进行分布式锁
        RLock lock = redissonClient.getLock(RedissonLockConstant.LOCK_SHORT_LINK_STATS_KEY + fullShortUrl);
        try {
            if (lock.tryLock()) {
                try {
                    // 更新t_link表中的total_系列
                    LambdaUpdateWrapper<TLink> eq = Wrappers.lambdaUpdate(TLink.class)
                            .setSql(statsRecord.getUvFirstFlag(), "total_uv = total_uv + 1")
                            .setSql(statsRecord.getUipFirstFlag(), "total_uip = total_uip + 1")
                            .setSql("total_pv = total_pv + 1")
                            .eq(TLink::getFullShortUrl, fullShortUrl)
                            .eq(TLink::getGid, gid)
                            .eq(TLink::getDelFlag, 0);
                    linkMapper.update(null, eq);
                    // 更新所有的统计信息
                    linkAccessStatsMapper.shortLinkStats(linkAccessStats);
                    linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStats);
                    linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStats);
                    linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStats);
                    linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStats);
                    linkOsStatsMapper.shortLinkOsState(linkOsStats);
                    linkStatsTodayMapper.shortLinkTodayState(linkStatsToday);
                    linkAccessLogsMapper.insert(linkAccessLogs);
                } catch (Exception ex) {
                    throw new ServiceException(BaseErrorCode.STATS_INSERT_ERROR);
                }
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public ShortLinkStatsRespDTO groupShortLinkStats(ShortLinkGroupStatsReqDTO reqDTO) {
        return linkStatsCacheManager.groupShortLinkStats(reqDTO);
    }

    @Override
    public IPage<ShortLinkStatsAccessRecordRespDTO> shortLinkStatsAccessRecord(ShortLinkStatsAccessRecordReqDTO requestParam) {
        // 查询基本访问信息
        LambdaQueryWrapper<TLinkAccessLogs> tLinkAccessLogsLambdaQueryWrapper = Wrappers.lambdaQuery(TLinkAccessLogs.class)
                .eq(TLinkAccessLogs::getGid, requestParam.getGid())
                .eq(TLinkAccessLogs::getFullShortUrl, requestParam.getFullShortUrl())
                .between(TLinkAccessLogs::getCreateTime, requestParam.getStartDate(), requestParam.getEndDate())
                .eq(TLinkAccessLogs::getDelFlag, 0)
                .orderByDesc(TLinkAccessLogs::getCreateTime);
        // 分页查询
        IPage<TLinkAccessLogs> shortLinkStatsAccessRecordReqDTO = linkAccessLogsMapper.selectPage(requestParam, tLinkAccessLogsLambdaQueryWrapper);
        // 将记录类型进行转换
        IPage<ShortLinkStatsAccessRecordRespDTO> convert = shortLinkStatsAccessRecordReqDTO.convert(item -> BeanUtil.toBean(item, ShortLinkStatsAccessRecordRespDTO.class));
        // 取出记录中的用户为集合
        List<String> list = convert.getRecords().stream().map(ShortLinkStatsAccessRecordRespDTO::getUser).toList();
        // 判断用户是否为新访客/老访客
        List<Map<String, Object>> maps = linkAccessLogsMapper.selectUvTypeByUsers(requestParam.getGid(), requestParam.getFullShortUrl(), requestParam.getStartDate(), requestParam.getEndDate(), list);
        // 将老访客/新访客放入分页记录中
        convert.getRecords().forEach(each -> {
            String s = maps.stream().filter(item -> Objects.equals(each.getUser(), item.get("user")))
                    .findFirst()
                    .map(item -> item.get("uvType"))
                    .map(Object::toString)
                    .orElse("老访客");
            each.setUvType(s);
        });
        return convert;
    }

    @Override
    public IPage<ShortLinkStatsAccessRecordRespDTO> groupShortLinkStatsAccessRecord(ShortLinkGroupStatsAccessRecordReqDTO requestParam) {
        // 查询基本访问信息
        LambdaQueryWrapper<TLinkAccessLogs> tLinkAccessLogsLambdaQueryWrapper = Wrappers.lambdaQuery(TLinkAccessLogs.class)
                .eq(TLinkAccessLogs::getGid, requestParam.getGid())
                .between(TLinkAccessLogs::getCreateTime, requestParam.getStartDate(), requestParam.getEndDate())
                .eq(TLinkAccessLogs::getDelFlag, 0)
                .orderByDesc(TLinkAccessLogs::getCreateTime);
        // 分页查询
        IPage<TLinkAccessLogs> shortLinkStatsAccessRecordReqDTO = linkAccessLogsMapper.selectPage(requestParam, tLinkAccessLogsLambdaQueryWrapper);
        // 将记录类型进行转换
        IPage<ShortLinkStatsAccessRecordRespDTO> convert = shortLinkStatsAccessRecordReqDTO.convert(item -> BeanUtil.toBean(item, ShortLinkStatsAccessRecordRespDTO.class));
        // 取出记录中的用户为集合
        List<String> list = convert.getRecords().stream().map(ShortLinkStatsAccessRecordRespDTO::getUser).toList();
        // 判断用户是否为新访客/老访客
        List<Map<String, Object>> maps = linkAccessLogsMapper.selectGroupUvTypeByUsers(requestParam.getGid(), requestParam.getStartDate(), requestParam.getEndDate(), list);
        // 将老访客/新访客放入分页记录中
        convert.getRecords().forEach(each -> {
            String s = maps.stream().filter(item -> Objects.equals(each.getUser(), item.get("user")))
                    .findFirst()
                    .map(item -> item.get("uvType"))
                    .map(Object::toString)
                    .orElse("老访客");
            each.setUvType(s);
        });
        return convert;
    }
}
